# https://www.cnblogs.com/feijian/p/4495281.html

STATUS_FORMAT_ERROR  = 1

# Trailer
TRR_CODE = 0x3B
# Image Block
IMG_CODE = 0x2C
# Extension
EXT_CODE = 0x21
# Graphic Control Extension
GC_EXT = 0xF9
# Application Extension
APP_EXT = 0xFF
# Comment Extension
CMT_EXT = 0xFE
# Plain Text Extension
TXT_EXT = 0x01

class GifFrame:
    def __init__(self, data:bytes, delay:int):
        self.data = data
        self.delay = delay

class GifHeader:
    def __init__(self, data:bytes, offset:int):
        globalColorTableFlag = (data[offset + 0x0A] & 0x80) != 0x00
        globalColorTableSize = (data[offset + 0x0A] & 0x07)

        # get size
        self.size = 0x0D;
        if globalColorTableFlag:
            self.size += (2 ** (globalColorTableSize + 1)) * 3

        self.data = data[offset:offset + self.size]

    def get_signature(self):
        return self.data[0:3].decode('utf8')
    
    def get_version(self):
        return self.data[3:6].decode('utf8')
    
    def get_width(self):
        return (self.data[6] & 0xff) + ((self.data[7] & 0xff) << 8)
    
    def get_height(self):
        return (self.data[8] & 0xff) + ((self.data[9] & 0xff) << 8)
    
    def get_global_color_table_flag(self):
        return (self.data[10] & 0x80) >> 7
    
    def get_color_resolution(self):
        return (self.data[10] & 0x70) >> 4

    def get_sort_flag(self):
        return (self.data[10] & 0x08) >> 3
    
    def get_size_of_global_color_table(self):
        return self.data[10] & 0x07
   
    def get_background_color_index(self):
        return self.data[11] & 0xFF

    def get_pixel_aspect_ratio(self):
        return self.data[12]
    
    def get_global_color_table(self):
        if self.get_global_color_table_flag() == 0:
            return [0]
        
        colors = [0] * (2 ** (self.get_size_of_global_color_table() + 1))
        for i in range(0, len(colors)):
            colors[i] = ((self.data[13 + (i * 3)] & 0xFF) << 16) + ((self.data[13 + (i * 3) + 1] & 0xFF) << 8) + (self.data[13 + (i * 3) + 2] & 0xFF)
        return colors
    
    def get_size(self):
        return self.size
       
class ImageBlock:
    def __init__(self, data:bytes, offset:int):
        localColorTableFlag = (data[offset + 0x09] & 0x80) != 0x00
        localColorTableSize = (data[offset + 0x09] & 0x07)

        # get size
        size = 0x0A;
        if localColorTableFlag:
            size += (2 ** (localColorTableSize + 1)) * 3;
        
        size += 1; # LZW Minimum Code Size
         
        # ImageData
        blockSize = data[offset + size] & 0xFF
        # print("blockSize", blockSize)
        size += 1
        while blockSize != 0x00:
            size += blockSize
            blockSize = data[offset + size] & 0xFF
            size += 1

        self.data = data[offset:offset + size]

    def get_image_separator(self):
        return self.data[0] & 0xFF
   
    def get_image_left_position(self):
        return (self.data[1] & 0xFF) + ((self.data[2] & 0xFF) << 8)
   
    def get_image_top_position(self):
        return (self.data[3] & 0xFF) + ((self.data[4] & 0xFF) << 8)
    
    def get_image_width(self):
        return (self.data[5] & 0xFF) + ((self.data[6] & 0xFF) << 8)
    
    def get_image_height(self):
        return (self.data[7] & 0xFF) + ((self.data[8] & 0xFF) << 8)
    
    def get_local_color_table_flag(self):
        return (self.data[9] & 0x80) >> 7
    
    def get_interlace_flag(self):
        return (self.data[9] & 0x40) >> 6
   
    def get_sort_flag(self):
        return (self.data[9] & 0x20) >> 5
    
    def get_reserved(self):
        return (self.data[9] & 0x18) >> 2
    
    def get_size_of_local_color_table(self):
        return self.data[9] & 0x03
    
    def get_local_color_table(self):
        if self.get_local_color_table_flag() == 0:
            return [0]
        
        colors = [0] * (2 ** self.get_size_of_local_color_table() + 1)
        for i in range(0, len(colors)):
            colors[i] = ((bytes[10 + (i * 3)] & 0xFF) << 16) + ((bytes[10 + (i * 3) + 1] & 0xFF) << 8) + (bytes[10 + (i * 3) + 2] & 0xFF)
        return colors
    
    def get_lzw_minimum_code_size(self):
        if self.get_local_color_table_flag() == 0:
            return self.data[10] & 0xFF
        else:
            return self.data[10 + (2 ** self.get_size_of_local_color_table() + 1) * 3] & 0xFF

class GraphicControlExtension:
    def __init__(self, data:bytes, offset:int):
        self.size = 8
        self.data = data[offset:offset + self.size]

    def get_extension_introducer(self):
        return self.data[0] & 0xFF
   
    def get_graphic_control_label(self):
        return self.data[1] & 0xFF
    
    def get_block_size(self):
        return self.data[2] & 0xFF
    
    def get_reserved(self):
        return (self.data[3] & 0xE0) >> 5
    
    def get_disposal_method(self):
        return (self.data[3] & 0x1C) >> 2
   
    def get_user_input_flag(self):
        return (self.data[3] & 0x02) >> 1
    
    def get_transparent_color_flag(self):
        return self.data[3] & 0x01
    
    def get_delay_time(self):
        return (self.data[4] & 0xFF) + ((self.data[5] & 0xFF) << 8)
    
    def get_transparent_color_index(self):
        return self.data[6] & 0xFF
    
    def set_transparent_color_flag_true(self):
        value = self.get_reserved() | self.get_disposal_method() | self.get_user_input_flag() | 0x01
        self.bytes[3] = value


class ApplicationExtension:
    def __init__(self, data:bytes, offset:int):
        blockSize = 0
        # get size
        self.size = 0x0E

        blockSize = data[offset + self.size] & 0xFF
        self.size += 1
        while blockSize != 0x00:
            self.size += blockSize
            blockSize = data[offset + self.size] & 0xFF
            self.size += 1

        self.data = data[offset:offset + self.size]
    
class CommentExtension:
    def __init__(self, data:bytes, offset:int):
        # get size
        self.size = 0x02

        blockSize = data[offset + self.size] & 0xFF
        self.size += 1
        while (blockSize != 0x00) :
            self.size += blockSize
            blockSize = data[offset + self.size] & 0xFF
            self.size += 1

        self.data = data[offset:offset + self.size]

    
class PlainTextExtension:
    def __init__(self, data:bytes, offset:int):
        # get size
        self.size = 0x0F

        blockSize = data[offset + self.size] & 0xFF
        self.size += 1
        while blockSize != 0x00:
            self.size += blockSize
            blockSize = data[offset + self.size] & 0xFF
            self.size += 1

        self.data = data[offset:offset + self.size]

class GifImageDecoder:
    def __init__(self):
        self.mGifHeader:GifHeader = None
        self.mWidth = 0
        self.mHeight = 0
        self.mOffset = 0
        self.mFrameCount = 0
        self.mGifFrames:list[GifFrame] = []
        self.mDelay = 0
        self.mLastDispose = 0
        self.mGcExt:GraphicControlExtension = None
        self.mDispose = 0
        self.mImageBlock:ImageBlock = None
        self.mFrameIndex = 0

        
    def read(self,data:bytes):
        self.mOffset = 0
        self.mGifHeader = GifHeader(data, self.mOffset)
        if self.mGifHeader.get_signature() != "GIF":
            return STATUS_FORMAT_ERROR;
        self.mOffset += self.mGifHeader.get_size()
        self.mWidth = self.mGifHeader.get_width()
        self.mHeight = self.mGifHeader.get_height()
        while data[self.mOffset] != TRR_CODE:
            if data[self.mOffset] == IMG_CODE:
                # ImageBlock
                self.mImageBlock = ImageBlock(data, self.mOffset)
                self.mOffset += len(self.mImageBlock.data)

                self.mFrameCount += 1
                # create new image to receive frame data
                mCurrentImage = self.extractImage()
                if self.mLastDispose > 0:
                    if self.mLastDispose == 3:
                        # use image before last
                        n = self.mFrameCount - 2
                        if n > 0:
                            mLastImage = self.getFrame(n - 1)
                        else:
                            mLastImage = None
                self.mGifFrames.append(GifFrame(mCurrentImage, self.mDelay)); # add image to frame
                self.resetFrame()
            elif data[self.mOffset] == EXT_CODE:
                if data[self.mOffset + 1] == GC_EXT:
                    # GraphicControlExtension
                    self.mGcExt = GraphicControlExtension(data, self.mOffset)
                    self.mOffset += self.mGcExt.size
                    self.mDispose = self.mGcExt.get_disposal_method()
                    if self.mDispose == 0:
                        self.mDispose = 1
                    self.mDelay = self.mGcExt.get_delay_time() * 10
                elif data[self.mOffset + 1] == APP_EXT:
                    appExt = ApplicationExtension(data, self.mOffset)
                    self.mOffset += appExt.size
                elif data[self.mOffset + 1] == CMT_EXT:
                    cmtExt = CommentExtension(data, self.mOffset)
                    self.mOffset += cmtExt.size
                elif data[self.mOffset + 1] == TXT_EXT:
                    txtExt = PlainTextExtension(data, self.mOffset)
                    self.mOffset += txtExt.size
                else:
                    raise Exception("Unknown extension")
            else:
                raise Exception("Unknown block")

    def getFrame(self):
        if self.mFrameCount <= 0:
            return None
        n = n % self.mFrameCount
        return self.mGifFrames[n].image

    def extractImage(self):
        ba = bytearray()
        ba += self.mGifHeader.data # 加入 Gif Header
        if self.mGcExt != None:
            if ((self.mWidth != self.mImageBlock.get_image_width() or self.mHeight != self.mImageBlock.get_image_height()) and
                    self.mGcExt.get_transparent_color_flag() == 0):
                self.mGcExt.set_transparent_color_flag_true()
            
            ba += self.mGcExt.data # 加入 Gif Graphic Control Extension

        ba += self.mImageBlock.data # 加入 Gif Image Block
        ba.append(0x3B) # 加入 Gif Trailer
        # write file gif
        with open(f"test_frame{self.mFrameIndex}_delay{self.mDelay}.gif", "wb") as f:
            f.write(ba)

        self.mFrameIndex += 1
        pass

    def resetFrame(self):
        self.mLastDispose = self.mDispose
        self.mDispose = 0
        self.mDelay = 0
        pass


def readBin(filePath)->bytes:
    with open(filePath, 'rb') as f:
        return f.read()
        


bindata= readBin('test2.gif')

decoder = GifImageDecoder()
decoder.read(bindata)
# offset = 0

# print("----------------GIF Header------------------")
# gifheader = GifHeader(bindata, offset)

# print("get_signature",gifheader.get_signature())
# print("get_version",gifheader.get_version())
# print("get_width",gifheader.get_width())
# print("get_height",gifheader.get_height())
# print("get_global_color_table_flag",gifheader.get_global_color_table_flag())
# print("get_color_resolution",gifheader.get_color_resolution())
# print("get_sort_flag",gifheader.get_sort_flag())
# print("get_size_of_global_color_table",gifheader.get_size_of_global_color_table())
# print("get_background_color_index",gifheader.get_background_color_index())
# print("get_pixel_aspect_ratio",gifheader.get_pixel_aspect_ratio())
# print("get_global_color_table",[f"#{hex(b)[2:].zfill(6)}" for b in gifheader.get_global_color_table()])


# print("----------------GIF ImageBlock------------------")

# offset = offset + gifheader.get_size()
# print("offset", offset)
# imageBlock = ImageBlock(bindata, offset)
# print("get_image_separator", imageBlock.get_image_separator())
# print("get_image_left_position", imageBlock.get_image_left_position())
# print("get_image_top_position", imageBlock.get_image_top_position())
# print("get_image_width", imageBlock.get_image_width())
# print("get_image_height", imageBlock.get_image_height())
# print("get_local_color_table_flag", imageBlock.get_local_color_table_flag()) 
# print("get_interlace_flag", imageBlock.get_interlace_flag())
# print("get_sort_flag", imageBlock.get_sort_flag())
# print("get_reserved", imageBlock.get_reserved())
# print("get_size_of_local_color_table", imageBlock.get_size_of_local_color_table())
# print("get_local_color_table",[f"#{hex(b)[2:].zfill(6)}" for b in imageBlock.get_local_color_table()])
# print("get_lzw_minimum_code_size",imageBlock.get_lzw_minimum_code_size())